# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: inkyu

from collections import namedtuple
import logging
import math

from coin.strategy.cheese.pricer.sidecar.l3 import SidecarL3Pricer
from coin.strategy.mm.calculator import MemcachedExponentialDecay, ExponentialDecay

L2Price = namedtuple('L2Price', ['timestamp', 'midp0', 'ema', 'target_price'])

PassivePrices = namedtuple('PassivePrices',
                           [
                               'askp',
                               'bidp',
                               'askp_pull',
                               'bidp_pull',
                               'askp_agg',
                               'bidp_agg',
                               'askt',
                               'bidt',
                           ])


class SidecarL2Pricer(object):
  def __init__(self, l2_config, *, strategy_logger):
    self._logger = logging.getLogger(__name__)

    self._l2_config = l2_config
    assert -10e-4 < self._l2_config.fee_pass < 20e-4, self._l2_config.fee_pass

    self._l3_pricers = [
        SidecarL3Pricer(product=l2_config.native_products[0],
                        vwap_long_sec=l2_config.vwap_width_sec),
        SidecarL3Pricer(product=l2_config.native_products[1],
                        vwap_long_sec=l2_config.volume_width_sec),
    ]

    if self._l2_config.true_book_qty_pass is not None:
      self._logger.info('Set true_book_qty_pass: %s', self._l2_config.true_book_qty_pass)
      self._l3_pricers[0].set_true_book_qty(self._l2_config.true_book_qty_pass)
    else:
      self._logger.info('Set true_book_price_pass: %s', self._l2_config.true_book_price_pass)
      self._l3_pricers[0].set_true_book_price(self._l2_config.true_book_price_pass)
    if self._l2_config.true_book_qty_ref is not None:
      self._logger.info('Set true_book_qty_ref: %s', self._l2_config.true_book_qty_ref)
      self._l3_pricers[1].set_true_book_qty(self._l2_config.true_book_qty_ref)

    self._strategy_logger = strategy_logger
    self._relpos = None
    self._premium = None
    self._prices = None
    # self._ema = MemcachedExponentialDecay(
    #  lifetime=self._l2_config.ema_lifetime_sec * 1e9,
    #  key='%s_%s_%s' % (self._l2_config.parent.key_memcached,
    #                    self._l2_config.key,
    #                    self._l2_config.ema_lifetime_sec))
    self._ema = ExponentialDecay(lifetime=self._l2_config.ema_lifetime_sec * 1e9)
    self.premium = None

  def set_relpos(self, relpos):
    if self._relpos != relpos and (relpos < -1 or relpos > 1):
      self._logger.warn('relpos: %s', relpos)
    self._relpos = relpos

  def update_ema_price(self, ema_price):
    self._l3_pricers[1].set_true_book_price(self._l2_config.true_book_price_pass * ema_price)

  def get_prices(self):
    return self._prices

  def calculate_price_passive(self, target_price):
    askp = (1. + self._l2_config.fee_pass + self._l2_config.edge) * target_price
    bidp = (1. - self._l2_config.fee_pass - self._l2_config.edge) * target_price

    if askp <= bidp:
      # self._logger.warning('Askp (%f) <= Bidp (%f)', askp, bidp)
      midp = (askp + bidp) / 2
      askp = self._l3_pricers[0].get_higher_tick(midp)
      bidp = self._l3_pricers[0].get_lower_tick(midp)

    askp_pull = askp * (1. - 3.e-4)
    askp_agg = askp * (1. + self._l2_config.edge_agg)
    bidp_pull = bidp * (1. + 3.e-4)
    bidp_agg = bidp * (1. - self._l2_config.edge_agg)

    assert askp > askp_pull, (askp_agg, askp, askp_pull, bidp_pull, bidp, bidp_agg)
    assert bidp_pull > bidp, (askp_agg, askp, askp_pull, bidp_pull, bidp, bidp_agg)
    assert askp > bidp, (askp_agg, askp, askp_pull, bidp_pull, bidp, bidp_agg)

    askp = self._l3_pricers[0].correct_ask_passive(askp, cut_in_line=True)
    bidp = self._l3_pricers[0].correct_bid_passive(bidp, cut_in_line=True)
    askp_agg = self._l3_pricers[0].correct_ask_passive(askp_agg, cut_in_line=False)
    bidp_agg = self._l3_pricers[0].correct_bid_passive(bidp_agg, cut_in_line=False)

    return PassivePrices(askp=askp,
                         bidp=bidp,
                         askp_pull=askp_pull,
                         bidp_pull=bidp_pull,
                         askp_agg=askp_agg,
                         bidp_agg=bidp_agg,
                         askt=self._l3_pricers[0].askt,
                         bidt=self._l3_pricers[0].bidt)

  def calculate(self, *, timestamp):
    target_price = None
    ema = None
    if self._l3_pricers[0].vwap_ready and self._l3_pricers[1].vwap_ready:
      power = math.tanh(0.05 * self._l3_pricers[1].vwap_short.qty
                        / self._l3_pricers[1].vwap_long.qty)
      weight = (100. - self._l2_config.target_offset
                - power * self._l2_config.target_intensity) / 100.
      # self._logger.info('Power: %s, weight: %s', power, weight)

      premium = self._l3_pricers[1].vwap_short.price / self._l3_pricers[0].vwap_short.price
      self._ema.set(premium, timestamp)
      ema = self._ema.get()
      if ema == 0:
        ema = premium
      self.premium = ema

      leaning = (1. + self._relpos * (self._l2_config.leaning * 0.00002))
      target_price = (
          self._l3_pricers[0].vwap_long.price + weight *
          (-self._l3_pricers[0].vwap_long.price + self._l3_pricers[1].midp / ema)) * leaning

    return L2Price(timestamp=timestamp,
                   ema=ema,
                   midp0=self._l3_pricers[0].midp,
                   target_price=target_price)

  def get_buy_price(self, exch, *, passive=False, adjust_to_tick=False):
    buy_price = None
    if self._l3_pricers[exch].book_ready:
      if passive:
        buy_price = self._l3_pricers[exch].bid0_plus_one
      else:
        buy_price = self._l3_pricers[exch].askt
        if adjust_to_tick:
          buy_price = self._l3_pricers[exch].correct_bid_aggressive(buy_price)
    return buy_price

  def get_sell_price(self, exch, *, passive=False, adjust_to_tick=False):
    sell_price = None
    if self._l3_pricers[exch].book_ready:
      if passive:
        sell_price = self._l3_pricers[exch].ask0_minus_one
      else:
        sell_price = self._l3_pricers[exch].bidt
        if adjust_to_tick:
          sell_price = self._l3_pricers[exch].correct_ask_aggressive(sell_price)
    return sell_price

  def _invalidate(self):
    self._prices = None

  def on_trade(self, exchange_id, trade, available):
    self._l3_pricers[exchange_id].on_trade(trade, available)

  def on_book(self, exchange_id, book, available):
    self._l3_pricers[exchange_id].on_book(book, available)

    prices = self._l3_pricers[exchange_id]
    if prices.book_ready:
      self._strategy_logger.write_book(exchange_id=exchange_id,
                                       product=self._l2_config.native_products[0].symbol,
                                       true_price=prices.true_book_price,
                                       ask0_price=prices.ask0,
                                       bid0_price=prices.bid0,
                                       ask0_qty=prices.ask0_qty,
                                       bid0_qty=prices.bid0_qty,
                                       askt_price=prices.askt,
                                       bidt_price=prices.bidt)

    if not all(self._l3_pricers[i].book_ready for i in [0, 1]):
      self._logger.warn('L3 pricers is not ready: %s', self._l2_config.key)
      self._invalidate()
      return False

    if abs(self._l3_pricers[0].timestamp - self._l3_pricers[1].timestamp) > 2e9 or not available:
      self._invalidate()
      return False

    self._prices = self.calculate(timestamp=book.timestamp)
    return True
